Effective Objective-C 2.0 笔记

第一章

1. Objective-C 使用的是消息结构而非函数调用,其区别在于:

  • 消息结构的语言,其运行时所应执行的代码由运行环境决定,编译完并不知道应该执行哪个方法,即运行时绑定
  • 函数调用的语言,由编译器决定,也就是编译完就知道应该执行那个方法

2.内存

1
NSString *someString = @"The String";

对象所占内存总是分配在堆空间(heap space),而绝不会分配在栈(stack)上。

1
NSString *anotherString = someString;

两个变量都指向此实例,这说明当前栈帧(stack frame)里分配了两块内存,每块内存的大小都能容下一枚指针(在32位架构的计算机上是4字节,64位是8字节),这两块内存里的值都一样,就是NSString实例的内存地址。

分配在中的内存必须直接管理,分配在上的用于保存变量的内存则会在其栈帧弹出时自动清理

3.使用对象会比使用结构体性能差

对象还需要额外的开销,例如分配及释放堆内存等

4.尽量使用@class 前向引用

当只需要声明类型时,用@class 会避免编译.h 里不必要的东西,从而减少编译时间

5.多用字面量语法

1
NSNumber *someNumber = @7;
1
2
3
4
5
6
id object1 = /*...*/
id object2 = /*...*/
id object3 = /*...*/
NSArray *arrayA = [NSArray arrayWithObjects:object1,object2,object3,nil];
NSArray *arrayB = @[object1,object2,object3];

如果object2 是nil,而arrayB 会crash ,但是arrayWithObjects 方法会依次处理各个参数,直到发现nil 为止,所以最终arrayA = @[ object1 ],类似的方法还有dictionaryWithObjectsAndKeys:

6.多用类型常量,少用#define预处理指令

例如定义一个动画时长

#define ANIMATION_DURATION 0.3

最好替换成

static const NSTimeInterval kAnimationDuration = 0.3;

static表示其作用域仅限于当前的目标文件中,如果不加的话,如果其他编译单元也声明了同名变量,则编译器会抛出异常

如果只是在.m里定义的话,最好加上k 前缀,如果类外可见的话以类名为前缀

1
2
extern NSString* const XXStringConstant;
NSString* const XXStringConstant = @"XXStringConstant";

此类常量放在全局符号表里,这里的XXStringConstant就是一个“一个常量,而这个常量是指针,指向NSString 对象

第二章

1.属性

  • @dynamic : 告诉编译器不需要生成存取方法
  • @synthesize : 合成属性 firstName = _firstName;
  • weak : 非拥有的关系,所指对象遭到摧毁,属性也会清空(nil)
  • unsafe_unretained : 当所指对象摧毁时,属性值不会自动清空

2.等同性

相同的对象必须具有相同的哈希值,但是两个哈希值相同的对象不一定相同

1
2
3
4
5
6
7
8
9
10
11
NSMutableSet * set = [NSMutableSet new];
NSArray *arr1 = @[@1,@2];
NSArray *arr2 = @[@1];
[set addobject:arr1];
[set addobject:arr2]; //set {(1,2),(1)}
[arr2 addObject:@(2)]; //set {(1,2),(1,2)} 哈希值改变了
NSSet *set2 = [set copy]; // set {(1,2)}

3.类族模式

类族模式可以把实现细节隐藏在一套简单的公共接口后面

1
+ (UIButton* )buttonWithType:(UIButtonType)type;

该方法所返回的对象,其类型取决于传于传入的按钮类型,不管是什么类型,都是继承与UIButton;

4.在既既有类中使用关联对象存放自定义数据 “关联对象(Associated Object )”

存储策略 (Storage Policy)

关联类型 等效的@property 属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatommic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy
1
void objc_setAssociatedObject(id object,void*key,id value,objc_AssociationPolicy policy)

以给定的键和策略为某对象设置关联对象值

1
id objc_getAssociatedObject(id object,void*key)

根据给定的键和策略为某对象设置关联对象值

1
void objc_removeAssociatedObjects(id object)

移除指定对象的全部关联对象

key: static void* associatedKey = @"associatedKey"

多次使用alert视图时,使用此方法较好

5.理解objc_msgSend 的作用

1
void objc_msgSend(id self, SEL cmd, ...)

这是个“参数个数可变的函数”,能接受两个或两个以上的参数。第一个参数代表接收者,第二个参数代表方法,后续参数就是消息中的那些参数

1
id returnValue = [someObject messageName:parameter];

编译器会将这个消息转换成如下函数:

1
id returnValue = objc_msgSend(someObject, @selecter(messageName:), parameter)

该方法需要在接收者所属的类中搜寻其方法列表,如果能找到与方法名字相符的方法,就跳至其实现代码,若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。最终还是没有找到的话,那就执行消息转发

objc_msgSend会将匹配结果缓存在快速映射表(fast map)里,每个类都有这样一块缓存,加快执行速度

6.理解消息转发机制

在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译期还无法确定类中到底会不会有某个方法的实现。当对象接收到无法解读的消息后,就会启动消息转发机制,程序员可经此过程告诉对象应该如何处理未知消息。

消息转发分为两大阶段:

  1. 第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个未知的选择子,这叫做“动态方法解析”
  2. 第二阶段涉及“完整的消息转发机制”。如果runtime 已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了,此时runtime 会请求接收者以其他手段来处理该消息。这又分为两小步:
    首先,请接收者看看有没有其他对象能处理这条消息,若有,则runtime 会把这条消息转给那个对象,于是消息转发结束,一切如常。若没有“备援的接收者”,则启动完成的消息转发机制,runtime 会把与消息有关的全部细节封装到NSIvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

1
+ (BOOL)resolveInstanceMethed:(SEL)selector;

表示这个类是否能新增一个实例方法用以处理此选择子。如果是类方法,runtime 会调用一个类似的方法,叫resolveClassMethod:

备援接收者

第二次处理未知选择子的机会,runtime 会询问类:能不能把这条消息转给其他接收者来处理。

1
- (id)forwardingTargetForSelector: (SEL)selector;

若当前接收者能找到备援对象,则将其返回,若找不到,返回nil。通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象的内部,可能还有一系列其他对象,该对象可经由此方法将能处理选择子的相关内部对象返回,在外界看来,好像是该对象亲自处理了消息。

完整的消息转发

如果转发算法走到这一步,那么唯一能做的就是启用完整的消息转发机制了,首先创建NSIvocation 对象,把与尚未处理的那条消息有关的全部细节封于其中,此对象包含选择子,目标及参数。在触发NSInvocation 对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。相当于只是改变了调用目标。

1
-(void)forwardInvocation:(NSInvocation *)invocation;

继承体系中的每个类都有机会处理此调用请求,直至NSObject,继而调用doesNotRecognizeSelector:以抛出异常,表示最终未能得到处理。

###消息转发全流程
屏幕快照 2016-10-24 下午5.11.39

方法调配技术 (Method swizzling)

类的方法列表会把选择子的名称映射到相关的方法实现上,使得“动态消息派发系统”能够根据此找到应该调用的方法,这些方法均以函数指针的形式来表示,这种指针叫做IMP,原型如下:

1
id (*IMP)(id, SEL, ...)

runtime提供了一些方法来操作这张表

1
2
3
Method originMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originMethod, swappedMethod);
1
2
3
4
5
- (NSString* )my_lowercaseString{
NSString* lowercaser = [self my_lowercaseString];
NSLog(@"%@ => %@",self,lowercase);
return lowercase;
}

将上面方法和NSString 的lowercaseString 方法互换,这段代码看上去会死循环,但是,在运行期间,my_lowercaseString 的选择子实际对应的是lowercaseString 。通常开发者可以在调试阶段利用这个方法来为那些“完全不知道实现细节”的方法增加日志记录功能,不宜滥用

7.理解 “类对象” 的用意

id类型定义如下:

1
2
3
typedef struct objc_object{
Class isa;// "is a"指针,定义了对象所属的类
} *id;

Class 对象定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct objc_class *Class
struct objc_class{
Class isa;// "is a"指针,定义了对象所属的类
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct obcj_cache *cache;
struct objc_protocol_list *protocols;
};

类对象所属的类型叫做元类,用来表述类对象本身所具备的元数据。每个类既有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

继承体系如下:

屏幕快照 2016-10-24 下午10.15.08

比较对象是否等同除了 isKindOfClass:还可以用比较类对象是否相同,原因在于,类对象是“单例”,每个类的Class 仅有一个实例。

1
2
3
4
id object = /* ... */;
if ([object class] == [SomeClass class]){
// 'object is an instance of SomeClass'
}

虽然能这样比较,但不推荐。例如,某个对象可能会把其收到的所有选择子都转发到另一个对象。这样的对象叫做“代理(proxy)“,此种对象均以NSProxy 为根类。
如果在这种对象上调用class 方法,那么返回的是代理对象本身(此类是NSProxy 的子类),而非接受代理的对象所属的类,然而改用isKindOfClass:就能把这条消息转给接受代理的对象。

8.Block

1.块的内部结构

块本身也是对象,在存放块对象的内存区域中,首个变量是指向Class 对象的指针,叫做isa ,其余内存里含有块对象正常运转所需的各种信息。

void* isa
int flags
int reserved
void ()(void ,…) invoke
struct * descriptor
捕获到的变量

其中最重要的就是invoke 变量,这是个函数指针,指向块的实现代码。函数原型至少要接受一个void* 型的参数,此参数代表块。
descriptor 变量是指向结构体的指针,其中声明了块对象的总体的大小,还声明了copy 与dispose 这两个辅助函数所对应的函数指针。
descriptor 对应结构如下:

unsigned long int reserved
unsigned long int size
void ()(void , void *) copy
void ()(void , void *) dispose

块还会把它所捕获的所有变量都拷贝一份,这些拷贝都放在descriptor 变量后面,捕获了多少个变量,就要占据多少内存空间。注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke 函数之所以把块当第一个参数传递进来,原因在于,执行块时,要从内存中把这些捕获的变量读出来。

2.全局块、栈块及堆块

定义块的时候,其所占内存区域是分配在占栈中,块只在定义它的那个范围内有效。

1
2
3
4
5
6
7
8
9
10
11
void (^Block)();
if (/* some condition*/){
block = ^{
NSLog(@"Block A");
};
}else{
block = ^{
NSLog(@"Block B");
}
}
block();

这段代码就有危险,定义在if 里面的Block 都分配在栈内存中。在离开了if 语句后,编译器有可能把分配在块的内存覆写掉,可能导致crash。
为解决此问题,可以给块对象发送copy 消息来拷贝,这样可以把块从栈复制到堆上,块就成了带引用计数的对象了。后续的拷贝操作都不会真的执行复制,只是递增块对象的引用计数。

1
2
3
4
5
6
7
8
9
10
11
void (^Block)();
if (/* some condition*/){
block = [^{
NSLog(@"Block A");
} copy];
}else{
block = [^{
NSLog(@"Block B");
} copy]
}
block();

3.全局块

1
2
3
void (^Block)() = ^{
NSLog(@"This is a block");
}

由于在编译期能确定块所需的全部信息,所以可以把它做成全局块。这是种优化技术。